/**
 * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */
import { DomEmitterMixin, Rect, verifyLicense } from '@ckeditor/ckeditor5-utils';
import BalloonPanelView from '../panel/balloon/balloonpanelview';
import IconView from '../icon/iconview';
import View from '../view';
import { throttle } from 'lodash-es';
import poweredByIcon from '../../theme/icons/project-logo.svg';
const ICON_WIDTH = 53;
const ICON_HEIGHT = 10;
// ⚠ Note, whenever changing the threshold, make sure to update the docs/support/managing-ckeditor-logo.md docs
// as this information is also mentioned there ⚠.
const NARROW_ROOT_HEIGHT_THRESHOLD = 50;
const NARROW_ROOT_WIDTH_THRESHOLD = 350;
const DEFAULT_LABEL = 'Powered by';
/**
 * A helper that enables the "powered by" feature in the editor and renders a link to the project's
 * webpage next to the bottom of the editable element (editor root, source editing area, etc.) when the editor is focused.
 *
 * @private
 */
export default class PoweredBy extends DomEmitterMixin() {
	/**
	 * Creates a "powered by" helper for a given editor. The feature is initialized on Editor#ready
	 * event.
	 *
	 * @param editor
	 */
	constructor(editor) {
		super();
		this.editor = editor;
		this._balloonView = null;
		this._lastFocusedEditableElement = null;
		this._showBalloonThrottled = throttle(this._showBalloon.bind(this), 50, { leading: true });
		editor.on('ready', this._handleEditorReady.bind(this));
	}
	/**
	 * Destroys the "powered by" helper along with its view.
	 */
	destroy() {
		const balloon = this._balloonView;
		if (balloon) {
			// Balloon gets destroyed by the body collection.
			// The powered by view gets destroyed by the balloon.
			balloon.unpin();
			this._balloonView = null;
		}
		this._showBalloonThrottled.cancel();
		this.stopListening();
	}
	/**
	 * Enables "powered by" label once the editor (ui) is ready.
	 */
	_handleEditorReady() {
		const editor = this.editor;
		const forceVisible = !!editor.config.get('ui.poweredBy.forceVisible');
		/* istanbul ignore next -- @preserve */
		if (!forceVisible && verifyLicense(editor.config.get('licenseKey')) === 'VALID') {
			return;
		}
		// No view means no body collection to append the powered by balloon to.
		if (!editor.ui.view) {
			return;
		}
		editor.ui.focusTracker.on('change:isFocused', (evt, data, isFocused) => {
			this._updateLastFocusedEditableElement();
			if (isFocused) {
				this._showBalloon();
			}
			else {
				this._hideBalloon();
			}
		});
		editor.ui.focusTracker.on('change:focusedElement', (evt, data, focusedElement) => {
			this._updateLastFocusedEditableElement();
			if (focusedElement) {
				this._showBalloon();
			}
		});
		editor.ui.on('update', () => {
			this._showBalloonThrottled();
		});
	}
	/**
	 * Creates an instance of the {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView balloon panel}
	 * with the "powered by" view inside ready for positioning.
	 */
	_createBalloonView() {
		const editor = this.editor;
		const balloon = this._balloonView = new BalloonPanelView();
		const poweredByConfig = getNormalizedConfig(editor);
		const view = new PoweredByView(editor.locale, poweredByConfig.label);
		balloon.content.add(view);
		balloon.set({
			class: 'ck-powered-by-balloon'
		});
		editor.ui.view.body.add(balloon);
		editor.ui.focusTracker.add(balloon.element);
		this._balloonView = balloon;
	}
	/**
	 * Attempts to display the balloon with the "powered by" view.
	 */
	_showBalloon() {
		if (!this._lastFocusedEditableElement) {
			return;
		}
		const attachOptions = getBalloonAttachOptions(this.editor, this._lastFocusedEditableElement);
		if (attachOptions) {
			if (!this._balloonView) {
				this._createBalloonView();
			}
			this._balloonView.pin(attachOptions);
		}
	}
	/**
	 * Hides the "powered by" balloon if already visible.
	 */
	_hideBalloon() {
		if (this._balloonView) {
			this._balloonView.unpin();
		}
	}
	/**
	 * Updates the {@link #_lastFocusedEditableElement} based on the state of the global focus tracker.
	 */
	_updateLastFocusedEditableElement() {
		const editor = this.editor;
		const isFocused = editor.ui.focusTracker.isFocused;
		const focusedElement = editor.ui.focusTracker.focusedElement;
		if (!isFocused || !focusedElement) {
			this._lastFocusedEditableElement = null;
			return;
		}
		const editableEditorElements = Array.from(editor.ui.getEditableElementsNames()).map(name => {
			return editor.ui.getEditableElement(name);
		});
		if (editableEditorElements.includes(focusedElement)) {
			this._lastFocusedEditableElement = focusedElement;
		}
		else {
			// If it's none of the editable element, then the focus is somewhere in the UI. Let's display powered by
			// over the first element then.
			this._lastFocusedEditableElement = editableEditorElements[0];
		}
	}
}
/**
 * A view displaying a "powered by" label and project logo wrapped in a link.
 */
class PoweredByView extends View {
	/**
	 * Created an instance of the "powered by" view.
	 *
	 * @param locale The localization services instance.
	 * @param label The label text.
	 */
	constructor(locale, label) {
		super(locale);
		const iconView = new IconView();
		const bind = this.bindTemplate;
		iconView.set({
			content: poweredByIcon,
			isColorInherited: false
		});
		iconView.extendTemplate({
			attributes: {
				style: {
					width: ICON_WIDTH + 'px',
					height: ICON_HEIGHT + 'px'
				}
			}
		});
		this.setTemplate({
			tag: 'div',
			attributes: {
				style: {
					display : 'none'
				},
				class: ['ck', 'ck-powered-by'],
				'aria-hidden': true
			}
		});
	}
}
function getBalloonAttachOptions(editor, focusedEditableElement) {
	const poweredByConfig = getNormalizedConfig(editor);
	const positioningFunction = poweredByConfig.side === 'right' ?
								getLowerRightCornerPosition(focusedEditableElement, poweredByConfig) :
								getLowerLeftCornerPosition(focusedEditableElement, poweredByConfig);
	return {
		target: focusedEditableElement,
		positions: [positioningFunction]
	};
}
function getLowerRightCornerPosition(focusedEditableElement, config) {
	return getLowerCornerPosition(focusedEditableElement, config, (rootRect, balloonRect) => {
		return rootRect.left + rootRect.width - balloonRect.width - config.horizontalOffset;
	});
}
function getLowerLeftCornerPosition(focusedEditableElement, config) {
	return getLowerCornerPosition(focusedEditableElement, config, rootRect => rootRect.left + config.horizontalOffset);
}
function getLowerCornerPosition(focusedEditableElement, config, getBalloonLeft) {
	return (visibleEditableElementRect, balloonRect) => {
		const editableElementRect = new Rect(focusedEditableElement);
		if (editableElementRect.width < NARROW_ROOT_WIDTH_THRESHOLD || editableElementRect.height < NARROW_ROOT_HEIGHT_THRESHOLD) {
			return null;
		}
		let balloonTop;
		if (config.position === 'inside') {
			balloonTop = editableElementRect.bottom - balloonRect.height;
		}
		else {
			balloonTop = editableElementRect.bottom - balloonRect.height / 2;
		}
		balloonTop -= config.verticalOffset;
		const balloonLeft = getBalloonLeft(editableElementRect, balloonRect);
		// Clone the editable element rect and place it where the balloon would be placed.
		// This will allow getVisible() to work from editable element's perspective (rect source).
		// and yield a result as if the balloon was on the same (scrollable) layer as the editable element.
		const newBalloonPositionRect = visibleEditableElementRect
		.clone()
		.moveTo(balloonLeft, balloonTop)
		.getIntersection(balloonRect.clone().moveTo(balloonLeft, balloonTop));
		const newBalloonPositionVisibleRect = newBalloonPositionRect.getVisible();
		if (!newBalloonPositionVisibleRect || newBalloonPositionVisibleRect.getArea() < balloonRect.getArea()) {
			return null;
		}
		return {
			top: balloonTop,
			left: balloonLeft,
			name: `position_${config.position}-side_${config.side}`,
			config: {
				withArrow: false
			}
		};
	};
}
function getNormalizedConfig(editor) {
	const userConfig = editor.config.get('ui.poweredBy');
	const position = userConfig && userConfig.position || 'border';
	return {
		position,
		label: DEFAULT_LABEL,
		verticalOffset: position === 'inside' ? 5 : 0,
		horizontalOffset: 5,
		side: editor.locale.contentLanguageDirection === 'ltr' ? 'right' : 'left',
		...userConfig
	};
}
